Completed
Pull Request — develop (#85)
by Xaver
01:09
created

labellayer.js ➔ ... ➔ projectNodes   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
nc 1
dl 0
loc 8
rs 9.4285
nop 1
1
define(['leaflet', 'rbush', 'helper'],
2
  function (L, rbush, helper) {
3
    'use strict';
4
5
    var labelLocations = [['left', 'middle', 0 / 8],
6
      ['center', 'top', 6 / 8],
7
      ['right', 'middle', 4 / 8],
8
      ['left', 'top', 7 / 8],
9
      ['left', 'ideographic', 1 / 8],
10
      ['right', 'top', 5 / 8],
11
      ['center', 'ideographic', 2 / 8],
12
      ['right', 'ideographic', 3 / 8]];
13
    var labelShadow;
14
    var bodyStyle = { fontFamily: 'sans-serif' };
15
    var nodeRadius = 4;
16
17
    var cFont = document.createElement('canvas').getContext('2d');
18
19
    function measureText(font, text) {
20
      cFont.font = font;
21
      return cFont.measureText(text);
22
    }
23
24
    function mapRTree(d) {
25
      return { minX: d.position.lat, minY: d.position.lng, maxX: d.position.lat, maxY: d.position.lng, label: d };
26
    }
27
28
    function prepareLabel(fillStyle, fontSize, offset, stroke) {
29
      return function (d) {
30
        var font = fontSize + 'px ' + bodyStyle.fontFamily;
31
        return {
32
          position: L.latLng(d.nodeinfo.location.latitude, d.nodeinfo.location.longitude),
33
          label: d.nodeinfo.hostname,
34
          offset: offset,
35
          fillStyle: fillStyle,
36
          height: fontSize * 1.2,
37
          font: font,
38
          stroke: stroke,
39
          width: measureText(font, d.nodeinfo.hostname).width
40
        };
41
      };
42
    }
43
44
    function calcOffset(offset, loc) {
45
      return [offset * Math.cos(loc[2] * 2 * Math.PI),
46
        -offset * Math.sin(loc[2] * 2 * Math.PI)];
47
    }
48
49
    function labelRect(p, offset, anchor, label, minZoom, maxZoom, z) {
50
      var margin = 1 + 1.41 * (1 - (z - minZoom) / (maxZoom - minZoom));
51
52
      var width = label.width * margin;
53
      var height = label.height * margin;
54
55
      var dx = {
56
        left: 0,
57
        right: -width,
58
        center: -width / 2
59
      };
60
61
      var dy = {
62
        top: 0,
63
        ideographic: -height,
64
        middle: -height / 2
65
      };
66
67
      var x = p.x + offset[0] + dx[anchor[0]];
68
      var y = p.y + offset[1] + dy[anchor[1]];
69
70
      return { minX: x, minY: y, maxX: x + width, maxY: y + height };
71
    }
72
73
    return L.GridLayer.extend({
74
      onAdd: function (map) {
75
        L.GridLayer.prototype.onAdd.call(this, map);
76
        if (this.data) {
77
          this.prepareLabels();
78
        }
79
      },
80
      setData: function (d) {
81
        this.data = d;
82
        this.updateLayer();
83
      },
84
      updateLayer: function () {
85
        if (this._map) {
86
          this.prepareLabels();
87
        }
88
      },
89
      prepareLabels: function () {
90
        var d = this.data;
91
92
        // label:
93
        // - position (WGS84 coords)
94
        // - offset (2D vector in pixels)
95
        // - anchor (tuple, textAlignment, textBaseline)
96
        // - minZoom (inclusive)
97
        // - label (string)
98
        // - color (string)
99
100
        var labelsOnline = d.online.map(prepareLabel(null, 11, 8, true));
101
        var labelsOffline = d.offline.map(prepareLabel('rgba(212, 62, 42, 0.9)', 9, 5, false));
102
        var labelsNew = d.new.map(prepareLabel('rgba(48, 99, 20, 0.9)', 11, 8, true));
103
        var labelsLost = d.lost.map(prepareLabel('rgba(212, 62, 42, 0.9)', 11, 8, true));
104
105
        var labels = []
106
          .concat(labelsNew)
107
          .concat(labelsLost)
108
          .concat(labelsOnline)
109
          .concat(labelsOffline);
110
111
        var minZoom = this.options.minZoom;
112
        var maxZoom = this.options.maxZoom;
113
114
        var trees = [];
115
116
        var map = this._map;
117
118
        function nodeToRect(z) {
119
          return function (n) {
120
            var p = map.project(n.position, z);
121
            return { minX: p.x - nodeRadius, minY: p.y - nodeRadius, maxX: p.x + nodeRadius, maxY: p.y + nodeRadius };
122
          };
123
        }
124
125
        for (var z = minZoom; z <= maxZoom; z++) {
126
          trees[z] = rbush(9);
127
          trees[z].load(labels.map(nodeToRect(z)));
128
        }
129
130
        labels = labels.map(function (n) {
131
          var best = labelLocations.map(function (loc) {
132
            var offset = calcOffset(n.offset, loc);
133
            var i;
134
135
            for (i = maxZoom; i >= minZoom; i--) {
136
              var p = map.project(n.position, i);
137
              var rect = labelRect(p, offset, loc, n, minZoom, maxZoom, i);
138
              var candidates = trees[i].search(rect);
139
140
              if (candidates.length > 0) {
141
                break;
142
              }
143
            }
144
145
            return { loc: loc, z: i + 1 };
146
          }).filter(function (k) {
147
            return k.z <= maxZoom;
148
          }).sort(function (a, b) {
149
            return a.z - b.z;
150
          })[0];
151
152
          if (best !== undefined) {
153
            n.offset = calcOffset(n.offset, best.loc);
154
            n.minZoom = best.z;
155
            n.anchor = best.loc;
156
157
            for (var i = maxZoom; i >= best.z; i--) {
158
              var p = map.project(n.position, i);
159
              var rect = labelRect(p, n.offset, best.loc, n, minZoom, maxZoom, i);
160
              trees[i].insert(rect);
161
            }
162
163
            return n;
164
          }
165
          return undefined;
166
        }).filter(function (n) {
167
          return n !== undefined;
168
        });
169
170
        this.margin = 16;
171
172
        if (labels.length > 0) {
173
          this.margin += labels.map(function (n) {
174
            return n.width;
175
          }).sort().reverse()[0];
176
        }
177
178
        this.labels = rbush(9);
179
        this.labels.load(labels.map(mapRTree));
180
181
        this.redraw();
182
      },
183
      createTile: function (tilePoint) {
184
        var tile = L.DomUtil.create('canvas', 'leaflet-tile');
185
186
        var tileSize = this.options.tileSize;
187
        tile.width = tileSize;
188
        tile.height = tileSize;
189
190
        if (!this.labels) {
191
          return tile;
192
        }
193
194
        var s = tilePoint.multiplyBy(tileSize);
195
        var map = this._map;
196
        bodyStyle = window.getComputedStyle(document.querySelector('body'));
197
        labelShadow = bodyStyle.backgroundColor.replace(/rgb/i, 'rgba').replace(/\)/i, ',0.7)');
198
199
        function projectNodes(d) {
200
          var p = map.project(d.label.position);
201
202
          p.x -= s.x;
203
          p.y -= s.y;
204
205
          return { p: p, label: d.label };
206
        }
207
208
        var bbox = helper.getTileBBox(s, map, tileSize, this.margin);
209
        var labels = this.labels.search(bbox).map(projectNodes);
210
        var ctx = tile.getContext('2d');
211
212
        ctx.lineWidth = 5;
213
        ctx.strokeStyle = labelShadow;
214
        ctx.miterLimit = 2;
215
216
        function drawLabel(d) {
217
          ctx.font = d.label.font;
218
          ctx.textAlign = d.label.anchor[0];
219
          ctx.textBaseline = d.label.anchor[1];
220
          ctx.fillStyle = d.label.fillStyle === null ? bodyStyle.color : d.label.fillStyle;
221
222
          if (d.label.stroke) {
223
            ctx.strokeText(d.label.label, d.p.x + d.label.offset[0], d.p.y + d.label.offset[1]);
224
          }
225
226
          ctx.fillText(d.label.label, d.p.x + d.label.offset[0], d.p.y + d.label.offset[1]);
227
        }
228
229
        labels.filter(function (d) {
230
          return tilePoint.z >= d.label.minZoom;
231
        }).forEach(drawLabel);
232
233
        return tile;
234
      }
235
    });
236
  });
237